react 组件在 unmounted 后进行 setState 操作的报错处理

最近在项目中遇到一个这样的问题,在 react 组件 unmounted 之后 setState 会报错

我们先来看个简单的例子,主要是为了重现一下问题

class Child extends Component {
  state = {
    ballName: ''
  }

  componentDidMount() {
    setTimeout(() => {
      this.setState({
        ballName: 'Ball Zhang'
      }, 1000)
    })
  }

  render() {
    return <span>Hello! {this.state.ballName}</span>
  }
}
class Parent extends Component {
  state = {
    isShowed: true
  }

  componentDidMount() {
    setTimeout(()=> {
      this.setState({
        isShowed: false
      })
    }, 300)
  }

  render() {
    const message = this.state.isShowed ? <Child /> : 'Bye!'
    return (
      <div>
         <span>{ message }</span>
      </div>
    )
  }
}

举的例子比较简略,主要是为了说明问题

在 Parent 组件中,300ms 之后移除了 Welcome 组件,但在 Child 组件里 1000ms 之后会改变 Welcome 组件的状态。这时候 React 会报出如下错误:

Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component.

这种错误情况一般出现在 react 组件已经从 DOM 中移除,但组件的 setState 还未执行完

我们在 react 组件中发送一些异步请求或者进行异步操作时, 就有可能会出现这样的问题,这个例子的 setState 其实也是异步操作

再举个例子,我们在 componentDidMount 中发送异步请求,当请求成功返回数据,我们调用 setState 改变组件的状态。但是当请求到达之前, 我们更换了页面或者移除了组件,就会报这个错误。这是因为虽然组件已经被移除,但是请求还在执行, 所以会报setState() on an unmounted component的错误

解决办法

现在我们已经知道问题出现的原因了,那应该怎么来解决呢?

实质上是要求我们在 react 组件被移除之前终止 setState 操作就行了

回到我们之前的例子,可以这样来做:

一种方式在子组件上设置定时器,可随时取消

componentDidMount() {
  // 把 setTimeout 保存在 timer 里
  this.timer = setTimeout(() => {
    this.setState({
      name: 'Ball Zhang'
    })
  }, 1000)
}

// 在组件将要被移除的时候,清除 timer
componentWillUnmount() {
  clearTimeout(this.timer)
}

类似的在处理 ajax 请求的时候也是这个套路, 在 componentWillUnmount 方法中终止 ajax 请求即可

componentWillMount() {
  this.xhr = $.ajax({
    // 请求的细节
  })
}

componentWillUnmount() {
  this.xhr.abort()
}

另一种方法是在父组件移出子组件之前增加延时,保证子组件有足够的时间完成 setState

componentDidMount() {
    setTimeout(()=> {
      this.setState({
        isShowed: false
      })
    }, 2000)
  }

至于采用哪种方式来解决这个 bug ,还得根据具体情境来决定